package com.trilead.ssh2.signature;
import com.trilead.ssh2.crypto.CertificateDecoder;
import com.trilead.ssh2.crypto.PEMStructure;
import com.trilead.ssh2.crypto.cipher.BlockCipher;
import com.trilead.ssh2.crypto.cipher.BlockCipherFactory;
import com.trilead.ssh2.crypto.cipher.CBCMode;
import com.trilead.ssh2.crypto.cipher.DES;
import com.trilead.ssh2.packets.TypesReader;
import org.mindrot.jbcrypt.BCrypt;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
/**
* An decoder that can read keys written in the 'new' OpenSSH format, generally identified with the header
* 'BEGIN OPENSSH PRIVATE KEY'.
* @author Michael Clarke
*/
abstract class OpenSshCertificateDecoder extends CertificateDecoder {
private final String keyAlgorithm;
OpenSshCertificateDecoder(String keyAlgorithm) {
super();
this.keyAlgorithm = keyAlgorithm;
}
@Override
public String getStartLine() {
return "-----BEGIN OPENSSH PRIVATE KEY-----";
}
@Override
public String getEndLine() {
return "-----END OPENSSH PRIVATE KEY-----";
}
@Override
public KeyPair createKeyPair(PEMStructure pemStructure) {
return null;
}
@Override
public KeyPair createKeyPair(PEMStructure pemStructure, String password) throws IOException {
TypesReader pemReader = new TypesReader(pemStructure.getData());
byte[] header = pemReader.readBytes(15);
if (!"openssh-key-v1".equals(new String(header, StandardCharsets.UTF_8).trim())) {
throw new IOException("Could not find openssh header in key");
}
String cipher = pemReader.readString();
String kdf = pemReader.readString();
byte[] kdfOptions = pemReader.readByteString();
int keyCount = pemReader.readUINT32();
// I can't actually find any test cases for multiple keys to know how they're used. OpenSSH doesn't even support
// this case, so I'm not going to look any further for now.
if (keyCount != 1) {
throw new IOException("Only single OpenSSH keys are supported");
}
/*byte[] publicKeys = */pemReader.readByteString(); //public keys can be parsed from a private key
byte[] privateKeys = pemReader.readByteString();
if ("bcrypt".equals(kdf)) {
if (password == null) {
throw new IOException("PEM is encrypted but password has not been specified");
}
TypesReader kdfReader = new TypesReader(kdfOptions);
byte[] salt = kdfReader.readByteString();
int rounds = kdfReader.readUINT32();
SshCipher sshCipher = SshCipher.getInstance(cipher);
privateKeys = decryptData(privateKeys, generateKayAndIvPbkdf2(password.getBytes(StandardCharsets.UTF_8), salt, rounds, sshCipher.getKeyLength(), sshCipher.getBlockSize()), sshCipher);
} else if (!"none".equals(cipher) || !"none".equals(kdf)) {
throw new IOException("Unexpected encryption method for key");
}
TypesReader privateKeyTypeReader = new TypesReader(privateKeys);
int checkNumber1 = privateKeyTypeReader.readUINT32();
int checkNumber2 = privateKeyTypeReader.readUINT32();
if (checkNumber1 != checkNumber2) {
throw new IOException("Check integers didn't match");
}
String keyType = privateKeyTypeReader.readString();
if (!keyType.equals(keyAlgorithm)) {
throw new IOException("Invalid key type: " + keyType);
}
try {
KeyPair keyPair = generateKeyPair(privateKeyTypeReader);
/*byte[] comment = */privateKeyTypeReader.readByteString(); // we don't need the key name/comment
for (int i = 0; i < pemReader.remain(); i++) {
if (i + 1 != pemReader.readByte()) {
throw new IOException("Incorrect padding on private keys");
}
}
return keyPair;
} catch (GeneralSecurityException ex) {
throw new IOException("Could not create key pair", ex);
}
}
abstract KeyPair generateKeyPair(TypesReader typesReader) throws GeneralSecurityException, IOException;
private static byte[] decryptData(byte[] encryptedData, byte[] keyAndIv, SshCipher sshCipher) {
byte[] key = new byte[sshCipher.getKeyLength()];
byte[] iv = new byte[sshCipher.getBlockSize()];
System.arraycopy(keyAndIv, 0, key, 0, key.length);
System.arraycopy(keyAndIv, key.length, iv, 0, iv.length);
BlockCipher cipher = sshCipher.createBlockCipher(key, iv, false);
byte[] decrypted = new byte[encryptedData.length];
for (int i = 0; i < encryptedData.length / cipher.getBlockSize(); i++) {
cipher.transformBlock(encryptedData, i * cipher.getBlockSize(), decrypted, i * cipher.getBlockSize());
}
return decrypted;
}
private static byte[] generateKayAndIvPbkdf2(byte[] password, byte[] salt, int rounds, int keyLength, int ivLength) {
byte[] keyAndIV = new byte[keyLength + ivLength];
new BCrypt().pbkdf(password, salt, rounds, keyAndIV);
return keyAndIV;
}
private enum SshCipher {
DESEDE_CBC(24, 8, "des-ede3-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("3des-cbc", encrypt, key, iv);
}
},
DES_CBC(8, 8, "des-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
DES des = new DES();
des.init(encrypt, key);
return new CBCMode(des, iv, encrypt);
}
},
AES128_CBC(16, 16, "aes-128-cbc", "aes128-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes128-cbc", encrypt, key, iv);
}
},
AES192_CBC(24, 16, "aes-192-cbc", "aes192-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes192-cbc", encrypt, key, iv);
}
},
AES256_CBC(32, 16, "aes-256-cbc", "aes256-cbc") {
@Override
BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt) {
return BlockCipherFactory.createCipher("aes256-cbc", encrypt, key, iv);
}
};
private final String[] sshCipherNames;
private final int keyLength;
private final int blockSize;
SshCipher(int keyLength, int blockSize, String cipherName, String... cipherAliases) {
this.keyLength = keyLength;
this.blockSize = blockSize;
String[] sshCipherNames = new String[1 + (null == cipherAliases ? 0 : cipherAliases.length)];
sshCipherNames[0] = cipherName;
if (null != cipherAliases) {
System.arraycopy(cipherAliases, 0, sshCipherNames, 1, cipherAliases.length);
}
this.sshCipherNames = sshCipherNames;
}
abstract BlockCipher createBlockCipher(byte[] key, byte[] iv, boolean encrypt);
public int getBlockSize() {
return blockSize;
}
public int getKeyLength() {
return keyLength;
}
public static SshCipher getInstance(String cipher) {
for (SshCipher instance : values()) {
for (String name : instance.sshCipherNames) {
if (name.equalsIgnoreCase(cipher)) {
return instance;
}
}
}
throw new IllegalArgumentException("Unknown Cipher: " + cipher);
}
}
}